﻿namespace HuwmanCode.ServiceProcess.DebugSupport

open System
open System.Configuration.Install
open System.Diagnostics
open System.IO
open System.IO.Pipes
open System.ServiceProcess
open System.Threading
open System.Runtime.InteropServices
open System.Reflection
open System.Windows.Forms
open System.ComponentModel.Composition
open HuwmanCode
open HuwmanCode.IO.Pipes
open HuwmanCode.Diagnostics
open HuwmanCode.Diagnostics.Logging
open HuwmanCode.ServiceProcess
open HuwmanCode.UserAccountControl

[<Export(typeof<IAttachDebugger>)>]
type internal DebuggerAttacher() =
    let attempt (action:unit -> 'a) maxRetries (interval:int) =
        let rec attemptAction retries =
            try
                Some (action ())
            with :? COMException -> 
                if retries < maxRetries then 
                    Thread.Sleep(interval)
                    attemptAction (retries + 1)
                else None
        attemptAction 1

    let getVisualStudioInstance processId =
        let instanceName = sprintf "!VisualStudio.DTE.10.0:%i" processId
        let vsComObj = Activator.GetObjects(fun name -> name = instanceName) |> Seq.nth 0
        vsComObj.Instance.Value :?> EnvDTE.DTE

    let asyncAttachDebugger (vs:EnvDTE.DTE) =
        async { do! Async.Sleep(500) //Wait for the service control manager to create the service process
                try
                    use pipe = new NamedPipeClientStream(".", Assembly.GetEntryAssembly().FullName, PipeDirection.InOut)
                    let pipeReader = new StreamReader(pipe)
                    let pipeWriter = new StreamWriter(pipe)
                    pipe.Connect(5000) //The service process should be waiting for a named pipe connection
                
                    let serviceProcessId = Int32.Parse(pipeReader.ReadLine())
                    let getServiceProcess () = seq { for proc in vs.Debugger.LocalProcesses do yield proc } |> Seq.find(fun proc -> proc.ProcessID = serviceProcessId)
                    let serviceProcess = attempt getServiceProcess 10 100 |> Option.get
                    attempt serviceProcess.Attach 10 100 |> ignore

                    pipeWriter.WriteLine("Proceed")
                    pipeWriter.Flush()

                    return true
                with _ -> return false }

    interface IAttachDebugger with
        member this.Attach debuggerProcess = 
            let showFailure reason = MessageBox.Show(sprintf "Failed to attach debugger, %s!" reason, "Failed to attach debugger!", MessageBoxButtons.OK, MessageBoxIcon.Error) |> ignore
            let getParentWindow (handle:int) = { new IWin32Window with member this.Handle = nativeint(handle) }
            let showFailureModal parentHWnd reason = MessageBox.Show(getParentWindow parentHWnd, sprintf "Failed to attach debugger, %s!" reason, "Failed to attach debugger!", MessageBoxButtons.OK, MessageBoxIcon.Error) |> ignore
            let installServiceProcess () =
                let installConfirmed () = 
                    MessageBox.Show("Failed to attach debugger, the service process is not installed! Install now?", "Failed to attach debugger", 
                        MessageBoxButtons.YesNo, MessageBoxIcon.Error) = DialogResult.Yes
                match Uac.ProcessElevation with
                | ProcessElevation.Default 
                | ProcessElevation.Elevated when installConfirmed () -> 
                    ManagedInstallerClass.InstallHelper([| Process.Current.MainModule.FileName;"-i" |])
                    true
                | _ -> false

            try
                let vs = getVisualStudioInstance debuggerProcess.Id
                if Base.GetServiceProcessInstalledStatus () = false && installServiceProcess () = false then 
                    showFailureModal vs.ActiveWindow.HWnd "no installed services were found"
                    false
                else
                    Base.AsyncStopAllServices () |> Async.RunSynchronously |> ignore
                    let asyncStartServices = async { let! startedServices = Base.AsyncStartAllServices ()
                                                     return startedServices |> List.length > 0 }
                    let asyncAttach = seq [asyncStartServices;asyncAttachDebugger vs] |> Async.Parallel
                    let success = Async.RunSynchronously(asyncAttach)

                    let noServicesStarted = success |> Array.nth 0 = false
                    let couldNotAttachDebugger = success |> Array.nth 1 = false
                    if noServicesStarted then showFailureModal vs.ActiveWindow.HWnd "could not start the services"
                    elif couldNotAttachDebugger then showFailureModal vs.ActiveWindow.HWnd "could not attach the debugger"

                    success |> Array.forall(fun s -> s = true)
            with e -> 
                showFailure e.Message
                false
